"use client"; import * as React from "react"; import { ChevronDown, ChevronRight, Minus, Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { DepartmentNode } from "@/lib/users/knox-service"; import { getDomainLabel, getDomainColor } from "./domain-constants"; interface DepartmentAssignment { id: number; departmentCode: string; assignedDomain: string; description?: string | null; } interface DepartmentTreeViewProps { departments: DepartmentNode[]; selectedDepartments: string[]; onSelectionChange: (selected: string[]) => void; assignments: DepartmentAssignment[]; className?: string; } interface TreeNodeProps { node: DepartmentNode; selectedDepartments: string[]; onToggle: (departmentCode: string) => void; expandedNodes: Set; onExpandToggle: (departmentCode: string) => void; assignments: DepartmentAssignment[]; level: number; } function TreeNode({ node, selectedDepartments, onToggle, expandedNodes, onExpandToggle, assignments, level }: TreeNodeProps) { const isExpanded = expandedNodes.has(node.departmentCode); const hasChildren = node.children.length > 0; // 현재 부서에 할당된 도메인 찾기 const assignment = assignments.find(a => a.departmentCode === node.departmentCode); // 현재 노드의 선택 상태 확인 const isSelected = selectedDepartments.includes(node.departmentCode); // 하위 노드들 중 선택된 것이 있는지 확인 (부분 선택 상태 표시용) const hasSelectedChildren = React.useMemo(() => { if (!hasChildren) return false; const getAllChildCodes = (dept: DepartmentNode): string[] => { const codes: string[] = []; dept.children.forEach(child => { codes.push(child.departmentCode); codes.push(...getAllChildCodes(child)); }); return codes; }; const childCodes = getAllChildCodes(node); return childCodes.some(code => selectedDepartments.includes(code)); }, [node, selectedDepartments, hasChildren]); const handleToggle = () => { onToggle(node.departmentCode); }; const handleExpandToggle = () => { if (hasChildren) { onExpandToggle(node.departmentCode); } }; return (
{/* 확장/축소 버튼 */}
{hasChildren ? ( ) : null}
{/* 체크박스 */}
*:first-child]:opacity-50" )} />
{/* 부서 정보 */}
{node.departmentName || node.departmentCode} {/* 할당된 도메인 표시 */} {assignment && ( {getDomainLabel(assignment.assignedDomain)} )}
{/* 부서 코드 */}
{node.departmentCode} {assignment?.description && ( • {assignment.description} )}
{/* 하위 노드들 */} {hasChildren && isExpanded && (
{node.children.map((child) => ( ))}
)}
); } export function DepartmentTreeView({ departments, selectedDepartments, onSelectionChange, assignments, className, }: DepartmentTreeViewProps) { const [expandedNodes, setExpandedNodes] = React.useState>(new Set()); // 부서 토글 핸들러 const handleToggle = (departmentCode: string) => { const findNode = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { for (const node of nodes) { if (node.departmentCode === code) return node; const found = findNode(node.children, code); if (found) return found; } return null; }; const getAllChildCodes = (node: DepartmentNode): string[] => { const codes: string[] = []; node.children.forEach(child => { codes.push(child.departmentCode); codes.push(...getAllChildCodes(child)); }); return codes; }; const targetNode = findNode(departments, departmentCode); if (!targetNode) return; const isCurrentlySelected = selectedDepartments.includes(departmentCode); let newSelected: string[]; if (isCurrentlySelected) { // 선택 해제: 해당 부서만 제거 (하위 부서는 유지, 상위 부서에도 영향 없음) newSelected = selectedDepartments.filter(code => code !== departmentCode); } else { // 선택: 해당 부서 + 모든 하위 부서 추가 const childCodes = getAllChildCodes(targetNode); const codesToAdd = [departmentCode, ...childCodes].filter(code => !selectedDepartments.includes(code)); newSelected = [...selectedDepartments, ...codesToAdd]; } onSelectionChange(newSelected); }; // 노드 확장/축소 핸들러 const handleExpandToggle = (departmentCode: string) => { const newExpanded = new Set(expandedNodes); if (newExpanded.has(departmentCode)) { newExpanded.delete(departmentCode); } else { newExpanded.add(departmentCode); } setExpandedNodes(newExpanded); }; // 전체 확장/축소 const handleExpandAll = () => { if (expandedNodes.size === 0) { const getAllCodes = (nodes: DepartmentNode[]): string[] => { const codes: string[] = []; nodes.forEach(node => { if (node.children.length > 0) { codes.push(node.departmentCode); codes.push(...getAllCodes(node.children)); } }); return codes; }; setExpandedNodes(new Set(getAllCodes(departments))); } else { setExpandedNodes(new Set()); } }; // 전체 선택/해제 const handleSelectAll = () => { if (selectedDepartments.length === 0) { // 전체 선택 const allCodes: string[] = []; const collectCodes = (nodes: DepartmentNode[]) => { nodes.forEach(node => { allCodes.push(node.departmentCode); collectCodes(node.children); }); }; collectCodes(departments); onSelectionChange(allCodes); } else { // 전체 해제 onSelectionChange([]); } }; return (
{/* 헤더 */}

조직도

{/* 트리 본문 */} {departments.length === 0 ? (
부서 정보가 없습니다
) : (
{departments.map((dept) => ( ))}
)}
{/* 푸터 */} {selectedDepartments.length > 0 && (
선택된 부서: {selectedDepartments.length}개
)}
); }